﻿using System;
using System.Data.SqlTypes;
using System.IO;
using Microsoft.SqlServer.Types;

namespace HashFoo.SqlServer.Spatial
{
    /// <summary>
    /// Wrapper of a SqlGeography.
    /// </summary>
    /// <remarks>
    /// Userful in that it will hide all the DB types and expose native .NET types.
    /// </remarks>
    public class Geography : ICloneable
    {
        /// <summary>
        /// The srid of WGS84
        /// </summary>
        const int Wgs84Srid = 4326;

        /// <summary>
        /// Create a new geography from a SqlGeography
        /// </summary>
        /// <param name="sqlGeography"></param>
        public Geography(SqlGeography sqlGeography)
        {
            InternalGeography = sqlGeography;
        }

        /// <summary>
        /// Gets the area in the unit of measure of the <see cref="Geography"/> spatial reference system.  Returns 0 if 
        /// Area doesn't make sense for the Geometry (like a Point).
        /// </summary>
        /// <remarks>
        /// For most reference systems, this will be returned in square meters, including WGS84 (4326).
        /// </remarks>
        public double Area
        {
            get { return InternalGeography.STArea().Value; }
        }

        /// <summary>
        /// Gets the <see cref="GeometryType"/> of the geography.
        /// </summary>
        public GeometryType GeometryType
        {
            get { return GeometryType.FromValue(InternalGeography.STGeometryType().Value); }
        }

        /// <summary>
        /// <c>true</c> if the Geography has rings.
        /// </summary>
        public bool HasRings
        {
            get { return InternalGeography.NumRings().IsNull == false; }
        }

        /// <summary>
        /// The underlying SqlGeography.
        /// </summary>
        public SqlGeography InternalGeography { get; private set; }

        /// <summary>
        /// Get the number of rings this geography has.
        /// </summary>
        public int NumberOfRings
        {
            get { return HasRings ? InternalGeography.NumRings().Value : 0; }
        }

        /// <summary>
        /// Get or set the SRID of the Geography.
        /// </summary>
        public int Srid
        {
            get { return InternalGeography.STSrid.Value; }
            set { InternalGeography.STSrid = new SqlInt32(value); }
        }

        /// <summary>
        /// Creates a square polygon from two opposing corner points.
        /// </summary>
        /// <param name="xMin"></param>
        /// <param name="yMin"></param>
        /// <param name="xMax"></param>
        /// <param name="yMax"></param>
        /// <param name="srid"></param>
        /// <returns></returns>
        public static Geography CreateSquarePolygon(
            double xMin, double yMin,
            double xMax, double yMax,
            int srid = Wgs84Srid)
        {
            var builder = new SqlGeometryBuilder();
            builder.SetSrid(srid);
            builder.BeginGeometry(OpenGisGeometryType.Polygon);
            builder.BeginFigure(yMin, xMin);
            builder.AddLine(yMin, xMax);
            builder.AddLine(yMax, xMax);
            builder.AddLine(yMax, xMin);
            builder.AddLine(yMin, xMin);

            builder.EndFigure();
            builder.EndGeometry();

            return FromGeometry(builder.ConstructedGeometry);
        }

        /// <summary>
        /// Creates a geography from a geometry
        /// </summary>
        /// <param name="srid"></param>
        /// <param name="geometry"></param>
        /// <returns></returns>
        public static Geography FromGeometry(SqlGeometry geometry, int? srid = null)
        {
            // STUnion will automagically correct any bad ring orientation
            // See: http://blogs.msdn.com/b/edkatibah/archive/2008/08/19/working-with-invalid-data-and-the-sql-server-2008-geography-data-type-part-1b.aspx
            var validGeom = geometry.MakeValid().STUnion(geometry.STStartPoint());

            // use any srid override first, then the srid of the geometry, then wgs84
            srid = srid ?? (geometry.STSrid.IsNull == false ? geometry.STSrid.Value : Wgs84Srid);

            // use the validGeom with correct ring orientation to 
            // create a SqlGeography.
            return new Geography(SqlGeography.STGeomFromText(validGeom.STAsText(), srid.Value));
        }

        /// <summary>
        /// Creates a point type geography from a x/y (lon,lat) pair.
        /// </summary>
        /// <param name="longitude"></param>
        /// <param name="latitude"></param>
        /// <param name="srid"></param>
        /// <returns></returns>
        public static Geography FromPoint(double longitude, double latitude, int srid = Wgs84Srid)
        {
            var point = SqlGeography.Point(latitude, longitude, srid);
            return new Geography(point);
        }

        /// <summary>
        /// Gets a Geography from a raw SqlServer UDT byte[] (the value you get if you read 
        /// a SqlGeography column value directly from SqlServer).
        /// </summary>
        /// <param name="serializedData"></param>
        /// <returns></returns>
        public static Geography FromSqlServer(byte[] serializedData)
        {
            using (var stream = new MemoryStream(serializedData)) return FromSqlServer(stream);
        }

        /// <summary>
        /// Gets a Geography from a raw SqlServer UDT stream (the value you get if you read 
        /// a SqlGeography column value directly from SqlServer).
        /// </summary>
        /// <param name="serializedData"></param>
        /// <returns></returns>
        public static Geography FromSqlServer(Stream serializedData)
        {
            var sqlGeography = new SqlGeography();
            using (var reader = new BinaryReader(serializedData))
            {
                sqlGeography.Read(reader);
                return new Geography(sqlGeography);
            }
        }

        /// <summary>
        /// Create a geography from well known text (wkt).
        /// </summary>
        /// <param name="wkt"></param>
        /// <param name="srid"></param>
        /// <returns></returns>
        public static Geography FromWkt(string wkt, int srid = Wgs84Srid)
        {
            // Geography is _really_ picky about ring orientation.
            // We're going to use a little hack here and piggy back on SqlGeometry
            // instead, then use it to fix things up, then create the SqlGeography.
            // See: http://blogs.msdn.com/b/edkatibah/archive/2008/08/19/working-with-invalid-data-and-the-sql-server-2008-geography-data-type-part-1b.aspx

            // a helper SqlGeometry, which isn't picky about ring orientation
            var geometryHelper = SqlGeometry.STGeomFromText(wkt.ToSqlChars(), srid);

            return FromGeometry(geometryHelper, srid);
        }

        public SqlGeometry AsSqlGeometry
        {
            get
            {
                var wkb = AsWkb();
                var geom = SqlGeometry.STGeomFromWKB(new SqlBytes(wkb), Srid);
                return geom;
            }
        }

        /// <summary>
        /// Gets the <see cref="Geography"/> as well known binary (WKB)
        /// </summary>
        /// <returns></returns>
        public byte[] AsWkb()
        {
            return InternalGeography.STAsBinary().Value;
        }

        /// <summary>
        /// Gets the WKT representation of the Geography.
        /// </summary>
        /// <returns></returns>
        public string AsWkt()
        {
            return InternalGeography.STAsText().ToSqlString().Value;
        }

        /// <summary>
        /// Gets the geography as WKT with Z and M data.
        /// </summary>
        /// <returns></returns>
        public string AsWktWithZAndM()
        {
            return InternalGeography.AsTextZM().ToSqlString().Value;
        }

        /// <summary>
        /// Creates a new object that is a copy of the current instance.
        /// </summary>
        /// <returns>
        /// A new object that is a copy of this instance.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        public object Clone()
        {
            var copy = InternalGeography.Serialize();
            return FromSqlServer(copy);
        }

        /// <summary>
        /// Checks if two Geographies are spatially equal.
        /// </summary>
        /// <param name="other"></param>
        /// <exception cref="ArgumentNullException">The geography to check is null.</exception>
        /// <returns></returns>
        public bool EqualsSpatially(Geography other)
        {
            if (other == null) throw new ArgumentNullException("other");
            return InternalGeography.STEquals(other.InternalGeography).Value;
        }

        /// <summary>
        /// Check if this geography is a specific type.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public bool IsInstanceOf(GeometryType type)
        {
            return InternalGeography.InstanceOf(type.Value).Value;
        }

        /// <summary>
        /// Gets a ring at a specific 1 based index.  Null if this geography doesn't support rings.
        /// </summary>
        /// <param name="index">The index of the ring, starting at 1</param>
        /// <returns></returns>
        public Geography RingAt(int index)
        {
            if (!HasRings) return null;
            if (index < 1) throw new ArgumentOutOfRangeException("index", "The index must be 1 based");
            if (index > (NumberOfRings)) throw new ArgumentOutOfRangeException("index", "The index is more than the number of rings");

            var ring = InternalGeography.RingN(index);
            return new Geography(ring);
        }

        /// <summary>
        /// Gets the geographic (taking into account the SRID) centroid of the <c>Geography</c>.
        /// </summary>
        public Point Centroid
        {
            get
            {
                var center = InternalGeography.EnvelopeCenter();
                return new Point
                {
                    X = center.Long.Value,
                    Y = center.Lat.Value,
                };
            }
        }
    }
}